On this page

Skip to content

Solving WSL 2 Docker File Permission Issues

In the early days, my Docker environment was set up on Windows. However, most tutorials and configuration resources online are based on Linux environments. Coupled with the path differences between Windows and Linux file systems (e.g., C:\ vs /mnt/c), I often spent a lot of time on conversion and debugging.

Furthermore, while accessing the Windows file system via WSL 2 is convenient, there is always a performance penalty for cross-OS file I/O. To pursue a more native Linux experience and better performance, I eventually migrated all my Docker Compose projects to WSL 2.

The Permission Hell I Encountered

After migrating to WSL 2, I got into the habit of using Visual Studio Code (VS Code) on Windows with the Remote - WSL extension, using it as my file browser and editor within WSL.

However, I ran into a tricky problem: I could not directly access files created after a Docker container started.

This is mainly because Docker containers usually run as root by default. Consequently, files mounted (Volume) to the host (WSL) also become owned by root. Since the user I log into WSL with is typically a standard user created during setup (e.g., wing), VS Code would throw a Permission Denied error when trying to read or write these root-owned files as a standard user.

For command-line gurus, a quick sudo vim or sudo nano might solve it. But as someone who relies heavily on the GUI, my development experience would be severely hampered if I couldn't just double-click to open and edit files in VS Code.

Solutions I Tried

I searched for various methods to solve this:

  1. Modify file permissions: Manually changing file permissions (chmod/chown) or setting the container User. The results were often incomplete, and sometimes I accidentally caused VS Code connection permission errors.

The Brute-Force Solution

Eventually, I couldn't take it anymore and decided to use the "nuclear option": I directly modified the WSL configuration file /etc/wsl.conf to change the default login user to root.

ini
# /etc/wsl.conf
[user]
default = root

WARNING

This is an extremely insecure practice. Changing the default user to root grants you full administrative privileges for all operations within WSL. If you accidentally execute a malicious script or make a mistake (e.g., rm -rf /), the consequences could be catastrophic. Please do not imitate this in production or critical work environments.

Since this is just my local development machine, the file permission issues disappeared, and I could happily edit any file with VS Code.

A Slightly Safer Solution: Dev Containers

Until recently, while discussing this frustration with Gemini, it suggested I try Dev Containers.

What are Dev Containers?

Dev Containers (Development Containers) is a VS Code extension that allows you to package your entire "development environment" into a Docker container.

What problems does this solve?

  1. Environment Consistency: No need to install Node.js, Python, etc., on WSL first; the environment is ready once the container starts.
  2. Automatic Identity Synchronization (Solving Permission Issues): This is the most critical point. Linux recognizes UIDs (numeric IDs) rather than account names. When Dev Containers start, they automatically modify the UID of the user inside the container to match the one outside in WSL (e.g., 1000). This means files you create inside the container appear as "yours" on the host, naturally avoiding Permission Denied issues. Note, however, that this mechanism cannot solve the case where "Docker Compose services force files to be created as root," which is why we need special configuration later.

Dev Containers in Action

Next, I will explain how to set up a basic Dev Container environment.

Creating a Workspace

Assume I use the directory /home/wing/docker as the workspace for my Dev Container.

Step 1: Initialize Dev Container Configuration

For an empty project, we can generate the configuration file using VS Code commands.

  1. Press F1 or Ctrl+Shift+P to open the Command Palette.

  2. Type and select Dev Containers: Add Dev Container Configuration Files....

    Add Dev Container Configuration Command

  3. A menu will appear asking which definition to use.

    • If it's a Node.js project, you can select Node.js.
    • If you just want a clean environment, you can select Ubuntu or Debian. Since my WSL is Ubuntu-24.04 and I just need it to build a Docker environment, I will select Show All Definitions... and then search for and select Ubuntu as a demonstration.
  4. Select the version (e.g., noble for Ubuntu 24.04).

  5. You will then be asked if you need to install additional features. Search for and check Docker (outside of Docker).

    TIP

    • Note that multiple options with the same name may appear in the search results. Be sure to select the version maintained by devcontainers (usually with an official badge) to ensure best compatibility.
    • Docker (outside of Docker) does not have Docker functionality itself; it simply creates a Docker client inside the container to control the external (Host) Docker engine.

    Search for Docker outside of Docker

  6. The Docker (outside of Docker) configuration options will appear:

    • Select or enter a Docker/Moby CLI version: Select latest (default).
    • Do you want to keep the feature defaults or configure options?: Select "Configure options".
    • Select boolean options for 'Docker (docker-outside-of-docker)':
      • installDockerBuildx: Check.
      • installDockerComposeSwitch: Check.
      • moby (Install OSS Moby...): Uncheck. I only need the standard Docker CLI to control the external Docker Desktop; I don't need the Moby OSS version.
    • Compose version to use for docker-compose: Select v2.
    • Include the following optional files/directories (.github/dependabot.yml): Uncheck. This is just a simple local development environment; no need for GitHub Dependabot automated dependency updates.

At this point, VS Code will create a .devcontainer folder in your project directory, containing the devcontainer.json configuration file.

json
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/ubuntu
{
    "name": "Ubuntu",
    // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
    "image": "mcr.microsoft.com/devcontainers/base:noble",
    "features": {
        "ghcr.io/devcontainers/features/docker-outside-of-docker:1": {
            "installDockerBuildx": true,
            "installDockerComposeSwitch": true,
            "version": "latest",
            "dockerDashComposeVersion": "v2"
        }
    },

    // Features to add to the dev container. More info: https://containers.dev/features.
    // "features": {},

    // Use 'forwardPorts' to make a list of ports inside the container available locally.
    // "forwardPorts": [],

    // Use 'postCreateCommand' to run commands after the container is created.
    // "postCreateCommand": "uname -a",

    // Configure tool-specific properties.
    // "customizations": {},

    // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
    "remoteUser": "root"
}

TIP

"remoteUser": "root" is commented out by default. If you are using Bind Mounts for Volumes, you must enable this line; otherwise, you won't be able to edit files with root permissions generated by Docker Compose.

WARNING

If you modify devcontainer.json, you must Rebuild for the changes to take effect.

Theoretically, a notification will pop up in the bottom right corner when you modify it, and you can click the button to execute. If you missed it, refer to "Step 2: Open in Container" below to run it again.

Step 2: Open in Container

  1. Press F1 or Ctrl+Shift+P.
  2. Type and select Dev Containers: Reopen in Container.

VS Code will start building the Docker Image (the first time takes longer) and start the container.

Step 3: Experience the Results

When the green status bar in the bottom left shows Dev Container: Ubuntu..., it means you have successfully entered the container!

You can now open the terminal, and you will find yourself inside the container.

To verify that this is a complete Docker development environment, let's try creating a compose.yml:

yaml
services:
  searxng:
    image: searxng/searxng:latest
    container_name: searxng
    ports:
      - "8080:8080"
    volumes:
      - ./volumes/searxng:/etc/searxng
    restart: unless-stopped

Do not start Docker Compose directly in this workspace; it is recommended to start it outside the Dev Container.

WARNING

This is mainly because I personally prefer using Bind Mounts (mounting local directories to the container), but Bind Mounts are prone to permission issues.

With docker-outside-of-docker (DooD), because the Dev Container only controls the external (Host) Docker Daemon via a Socket, when you send a command to the Daemon to mount ./volumes, the Daemon looks for the path from the Host's perspective, not the Dev Container's internal path. This leads to path mapping errors, where files created after the container starts cannot be accessed correctly or result in empty directories.

For details, refer to the official documentation: Using Docker from Docker (Docker-outside-of-Docker)

However, if you use Named Volumes, you can avoid the path resolution issues between Host/Container caused by Bind Mounts in DooD. You can even add "postCreateCommand": "docker compose up -d" in devcontainer.json for one-click setup. It feels like I, who insists on using Bind Mounts, should be phased out.

Here is a comparison:

If we are outside the Dev Container (the original WSL workspace), we find that we cannot directly edit settings.yml under volumes due to permission issues:

Permission Denied

But inside the Dev Container, we can open and edit the file normally:

Normal access inside Dev Container

When VS Code with a Dev Container is open, checking the Docker Container list will show an extra container (like hungry_mccarthy in the image below); this is the Dev Container itself:

Dev Container list

And when you close that VS Code window, this Dev Container will automatically stop:

Dev Container automatically stopped

Another Option: Integrate Dev Container directly into Docker Compose

If you feel that creating a .devcontainer folder and maintaining extra configuration files for Dev Containers is too troublesome, or if you want this development environment to be visible directly in the project's compose.yml, you can adopt the "manually add a tool container" approach.

The concept here is: add an extra container specifically for running VS Code (let's call it vscode-editor) in compose.yml, put it on the same network as other services (like databases, Redis), so you can solve file permission issues while debugging other services directly from within the container.

Step 1: Modify compose.yml

Add a vscode-editor service to compose.yml:

yaml
services:
  # ... Existing services (searxng, database, etc.) ...

  vscode-editor:
    # Use the official Microsoft-maintained Dev Container base image (same as standard Dev Container)
    image: mcr.microsoft.com/devcontainers/base:ubuntu-24.04
    container_name: vscode-editor
    # Keep the container running, waiting for us to connect
    command: sleep infinity
    # Start as root here, the most brute-force way to solve mount permission issues
    user: root
    volumes:
      # Determine the path range to mount into the container; can be a single project (./) or a common parent directory for multiple services (see Tip below)
      - ./:/workspace
    working_dir: /workspace
    # Ensure it joins the same network for easy pinging or connecting to other containers
    networks:
      - default

TIP

Regarding how large the Workspace scope should be: I don't have a standard answer, but I'll share my personal thoughts:

  • If there are many services on the same network: I would consider mounting the smallest common parent directory that covers these services. This makes compose.yml cleaner, avoids listing a long list of volumes, and allows managing multiple related services in the same VS Code window.
  • If there are few services on the same network: For example, if only one or two containers need frequent modification, just list the specific folders. This keeps the environment as simple as possible and avoids accidentally touching unrelated files inside the container.

TIP

If you don't want these "development tool" container settings (pollution) to mix into the production compose.yml, you can move the vscode-editor configuration block to compose.override.yml.

Docker Compose automatically reads and merges the content of the override file upon startup, keeping the main configuration file clean (containing only production services) while allowing seamless use of Dev Containers in the development environment.

Start the project:

bash
docker compose up -d

Step 2: Attach to a Running Container

After starting, this vscode-editor container will wait in the background. Now, we connect to it via VS Code:

  1. Press F1 or Ctrl+Shift+P to open the Command Palette.

  2. Type and select Dev Containers: Attach to Running Container....

    Attach to running container

  3. Select the vscode-editor container we just created from the menu.

VS Code will open a new window and connect to the inside of the container. Then, select Open Folder and enter the path /workspace we mounted earlier.

Step 3: Fix Permission Settings (Configure devcontainer.json)

Although user: root is already specified in the YAML, to ensure that VS Code extensions and terminal behavior are consistent, it is recommended to create a configuration file for this container.

  1. In the connected window, press F1.

  2. Search for and select Dev Containers: Open Container Configuration File.

    Open attached container configuration file

    Attached image

  3. VS Code will create a dedicated configuration for this "attached container." Add "remoteUser": "root" inside it:

json
{
    "workspaceFolder": "/workspace",
    "remoteUser": "root"
}

This way, all modifications you make to files within /workspace inside the container will be executed as root, perfectly solving the permission issues generated by other Docker Compose services.

TIP

What happened to the "Rebuild" command? If you saw the rebuild prompt after editing devcontainer.json for the first time but can't find it in the command palette later, don't worry, this is normal.

If you modify the settings (e.g., switching remoteUser) and want to apply them, the simplest way is to press F1 and execute Developer: Reload Window. The settings will take effect after reconnecting.

The biggest advantage of this approach is: The development environment itself is part of the infrastructure, ready to use at any time without extra initialization steps.

Alternative: Docker Desktop Built-in Editor

Besides Dev Containers, if you only need to quickly modify a few files, the Docker Desktop GUI now supports editing files inside containers directly:

Docker Desktop built-in editor

Conclusion

Although I still used root with Dev Containers, which doesn't quite reflect the original intent of Dev Containers (User Mapping), at least this limits the root execution to the Dev Container itself rather than the entire WSL being root. Furthermore, the container does not default to reading files outside the Dev Container scope, making permissions relatively safer (hopefully).

Changelog

  • 2026-01-19 Initial document creation.
  • 2026-01-22 Added another approach for integrating Dev Containers into Docker Compose.
  • 2026-02-04 Added suggestions regarding the use of compose.override.yml.